/*
 *   The contents of this file are subject to the Mozilla Public License
 *   Version 1.1 (the "License"); you may not use this file except in
 *   compliance with the License. You may obtain a copy of the License at
 *   http://www.mozilla.org/MPL/
 *
 *   Software distributed under the License is distributed on an "AS IS"
 *   basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 *   License for the specific language governing rights and limitations
 *   under the License.
 *
 *   The Original Code is Matra - the DTD Parser.
 *
 *   The Initial Developer of the Original Code is Conrad S Roche.
 *   Portions created by Conrad S Roche are Copyright (C) Conrad 
 *   S Roche. All Rights Reserved.
 *
 *   Alternatively, the contents of this file may be used under the terms
 *   of the GNU GENERAL PUBLIC LICENSE Version 2 or any later version
 *   (the  "[GPL] License"), in which case the
 *   provisions of GPL License are applicable instead of those
 *   above.  If you wish to allow use of your version of this file only
 *   under the terms of the GPL License and not to allow others to use
 *   your version of this file under the MPL, indicate your decision by
 *   deleting  the provisions above and replace  them with the notice and
 *   other provisions required by the GPL License.  If you do not delete
 *   the provisions above, a recipient may use your version of this file
 *   under either the MPL or the GPL License."
 *
 *   [NOTE: The text of this Exhibit A may differ slightly from the text of
 *   the notices in the Source Code files of the Original Code. You should
 *   use the text of this Exhibit A rather than the text found in the
 *   Original Code Source Code for Your Modifications.]
 *
 * Created: Conrad S Roche <derupe at users.sourceforge.net>,  20-Sep-2003
 */
package com.conradroche.matra.parser;

import com.conradroche.dtd.decl.Notation;
import com.conradroche.dtd.parser.NotationReader;
import com.conradroche.matra.data.DTDData;
import com.conradroche.matra.exception.DTDSyntaxException;

/**
 * Class to read a Notation declaration.
 *
 * @author Conrad Roche
 */
public class NotationReaderImpl implements NotationReader {
	
	/**
	 * Token identifying a notation declaration.
	 */
	private static final String NOTATION_START = "<!NOTATION";
	private static final String SYSTEM = "SYSTEM";
	private static final String PUBLIC = "PUBLIC";

	/**
	 * List of valid chars in a public identifier.
	 */
	private static final String VALID_PUBID_CHAR = " \r\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-'()+,./:=?;!*#@$_%"; //#x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%]

	/**
	 * NotationReaderImpl Constructor.
	 */
	public NotationReaderImpl() {
		super();
	}

	/**
	 * Read the notation declaration from the data stream.
	 * 
	 * @param data The stream from which to read the comment.
	 * 
	 * @return The comment.
	 * 
	 * @throws DTDSyntaxException If the Notation 
	 * 			declaration contains a syntax error. 
	 */
	public Notation readNotation(DTDData data) throws DTDSyntaxException {
		
		//[82]    NotationDecl    ::=    '<!NOTATION' S Name S (ExternalID | PublicID) S? '>'
		//[83]    PublicID    ::=    'PUBLIC' S PubidLiteral  
		//[75]    ExternalID    ::=    'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral  
		//[11]    SystemLiteral    ::=    ('"' [^"]* '"') | ("'" [^']* "'")  
		//[12]    PubidLiteral    ::=    '"' PubidChar* '"' | "'" (PubidChar - "'")* "'" 
		//[13]    PubidChar    ::=    #x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%]
		//CR: TODO: check for constructs 11, 12 & 13 too. 

		//NotationDecl := '<!NOTATION' S name S (('SYSTEM' S sysId) | ('PUBLIC' S pubId S sysId?)) S? '>'
		
		if(!isNotationStart(data)) {
			return null;
		}
		
		Notation notation = new com.conradroche.matra.decl.Notation(); 
		
		//CR: TODO: It will be better if all these data. methods throw exception on end of stream
		// would be better than checking eof after every read.
		
		//CR: TODO: DTDData::getNextNameToken() throws EOFException InvalidNameException ?
		
		data.getNextToken(); // '<!NOTATION'
		data.skipWhiteSpace(); // S
		
		//CR: TODO: Move this eod check to the DTDData read methods.
		if( data.endOfData() ) {
			throw new DTDSyntaxException("Unexpected end of data stream encountered at location " + data.getCurrentLocation() + " - was expecting Notation name.");
		}
		
		//CR: TODO: Its better to get the next "token" and then check if its a valid name -
		//will be able to give more appropriate error messages then.
		String name = data.getNextName(); // name
		if((name == null || name.length() == 0)) {
			
			throw new DTDSyntaxException("The Notation Name must be a 'Name'. Invalid char '" + 
					data.lookNextChar() + "' encountered in the Notation Name.");
		}
		
		notation.setNotationName(name);
		
		data.skipWhiteSpace(); // S
		
		//CR: TODO: Move this eod check to the DTDData read methods.
		if( data.endOfData() ) {
			throw new DTDSyntaxException("Unexpected end of data stream encountered at location " + data.getCurrentLocation() + " - was expecting 'SYSTEM' or 'PUBLIC'.");
		}

		String token = data.getNextToken(); // 'SYSTEM' | 'PUBLIC'
		data.skipWhiteSpace(); //S
		
		//CR: TODO: Move this eod check to the DTDData read methods.
		if( data.endOfData() ) {
			throw new DTDSyntaxException("Unexpected end of data stream encountered at location " + data.getCurrentLocation() + " - was expecting quote char.");
		}

		if(token.equals(SYSTEM)) {
			readSystemNotation(data, notation);
		} else if(token.equals(PUBLIC)) {
			readPublicNotation(data, notation, name);
		} else {
			throw new DTDSyntaxException("Got invalid token while parsing a Notation declaration - " + token +
						" - was expecting either a 'SYSTEM' or 'PUBLIC'.");
		}
		
		return notation;
	}

	/**
	 * @param data
	 * @param notation
	 * @param name
	 * @throws DTDSyntaxException
	 */
	private void readPublicNotation(DTDData data, Notation notation, String name)
		throws DTDSyntaxException {
		char quot = data.getNextChar();
		if(!isQuotChar(quot)) {
			throw new DTDSyntaxException("A public literal should be enclosed in quotes (\' or \"); found invalid char - '"
						+ quot + "'.");
		}
		
		//CR: TODO: Move this eod check to the DTDData read methods.
		if( data.endOfData() ) {
			throw new DTDSyntaxException("Unexpected end of data stream encountered at location " + data.getCurrentLocation() + " - was expecting notation's public identifier.");
		}
		
		String pubId = data.getNextToken(quot); // pubId
		checkPubidLiteral(pubId);
		notation.setPublicIdentifier(pubId); 
		
		data.skipWhiteSpace(); //S
		
		//CR: TODO: Move this eod check to the DTDData read methods.
		if( data.endOfData() ) {
			throw new DTDSyntaxException("Unexpected end of data stream encountered at location " + data.getCurrentLocation() + " - was expecting '>'.");
		}
		
		if(data.lookNextChar() == '>') {
			data.getNextChar();
		} else {
			String sysId = data.getNextToken();
			notation.setSystemIdentifier(sysId); 
			data.skipWhiteSpace(); //S
			
			//CR: TODO: Move this eod check to the DTDData read methods.
			if( data.endOfData() ) {
				throw new DTDSyntaxException("Unexpected end of data stream encountered at location " + data.getCurrentLocation() + " - was expecting '>'.");
			}
		
			if(data.lookNextChar() != '>') {
				throw new DTDSyntaxException("Got invalid char while parsing a Notation declaration (" + 
								name + ") - "  +
								data.lookNextChar() + " - was expecting '>'.");
			} else {
				data.getNextChar();
			}
		}
	}

	/**
	 * @param data
	 * @param notation
	 * @throws DTDSyntaxException
	 */
	private void readSystemNotation(DTDData data, Notation notation)
		throws DTDSyntaxException {
		char quot = data.getNextChar();
		if(!isQuotChar(quot)) {
			throw new DTDSyntaxException("A system literal should be enclosed in quotes (\' or \"); found invalid char - '"
						+ quot + "'.");
		}
		
		//CR: TODO: Move this eod check to the DTDData read methods.
		if( data.endOfData() ) {
			throw new DTDSyntaxException("Unexpected end of data stream encountered at location " + data.getCurrentLocation() + " - was expecting the notation's system identifier.");
		}
		
		String sysId = data.getNextToken(quot); //sysId
		notation.setSystemIdentifier(sysId); 
		
		data.skipWhiteSpace(); //S
		
		//CR: TODO: Move this eod check to the DTDData read methods.
		if( data.endOfData() ) {
			throw new DTDSyntaxException("Unexpected end of data stream encountered at location " + data.getCurrentLocation() + " - was expecting '>'.");
		}
		
		data.getNextChar(); // '>'
	}

	/**
	 * Checks if the specified string follows the PubidLiteral construct.
	 * 
	 * @param pubId The string to check.
	 * 
	 * @throws DTDSyntaxException If the string does not follow the PubidLiteral construct.
	 */
	private void checkPubidLiteral(String pubId) throws DTDSyntaxException {
//		[12]    PubidLiteral    ::=    '"' PubidChar* '"' | "'" (PubidChar - "'")* "'" 

		if(pubId == null) {
			throw new DTDSyntaxException("Missing PubidLiteral in Notation declaration.");
		}
		
		char c;
		
		for(int i = 0; i < pubId.length(); i++) {
			c = pubId.charAt(i);
			if(!isPubidChar(c)) {
				throw new DTDSyntaxException("Invalid PubidLiteral in Notation declaration. Found invalid char '" + 
							c + "'.");
			}
		}
	}

	/**
	 * Checks if the char follows the PubidChar construct.
	 * 
	 * @param c The char to check.
	 * 
	 * @return Returns <code>true</code> if the char follows the PubidChar 
	 * 			construct; <code>false</code> otherwise.
	 */
	private boolean isPubidChar(char c) {
//		[13]    PubidChar    ::=    #x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%]

		if(VALID_PUBID_CHAR.indexOf(c) != -1) {
			return true; 
		}

		return false;
	}

	/**
	 * Checks if the char specified is 
	 * a quotation mark - either single (')
	 * or double (").
	 * 
	 * @param c The char to check.
	 * 
	 * @return <code>true</code> is the 
	 * 		specified char is a quote
	 * 		char; <code>false</code>
	 * 		otherwise.
	 */
	private boolean isQuotChar(char c) {
		
		if(c == '\'' || c == '\"') {
			return true;
		}
		
		return false;
	}

	/**
	 * Checks if there is a notation declaration at the 
	 * beginning of the data.
	 * 
	 * @param data The data to be parsed.
	 * 
	 * @return <code>true</code> if the current location
	 * 	of the data has a comment; <code>false</code> otherwise.
	 */
	public boolean isNotationStart(DTDData data) {
		
		if(data == null || data.endOfData()) {
			return false;
		}
		
		return data.nextTokenEquals(NOTATION_START);
	}
}
